<?php

namespace app\service;


/**
 * Redis 分布式锁
 * redis-version: >2.6.12
 * EX seconds - 设置指定的到期时间，单位为秒
 * PX milliseconds - 设置指定到期时间，单位为毫秒
 */
class RedisMutexLock
{
    protected $redis;
    protected $acquireTimeout = 2; //最大阻塞时间s

    public function __construct() {
        try {
            $this->redis = redis();
        } catch (\Exception $e) {
            die('error-RedisConnect' . $e->getCode() . $e->getMessage());
        }
    }

    /**
     * 获取分布式锁（加锁）
     * @param $lockKey
     * @param $requestId
     * @param int $expireTime
     * @param bool $isNegative
     * @return bool
     */
    public function tryGetDistributedLock($lockKey, $requestId, int $expireTime = 5, bool $isNegative = false): bool
    {
        if ($isNegative) { //悲观锁
            /**
             * 悲观锁：循环阻塞式锁取，阻塞时间为2s
             */
            $endTime = microtime(true) * 1000 + $this->acquireTimeout * 1000;
            while (microtime(true) * 1000 < $endTime) {
                $acquired = $this->redis->set($lockKey, $requestId, ['nx', 'ex' => $expireTime]);
                if ($acquired) {
                    return true;
                }
                usleep(500);
                debugLog("$requestId: 尝试重新获取锁", 'redis');
            }
        } else { //乐观锁
            /**
             * 乐观锁：只尝试一次，成功返回true，失败返回false
             */
            $ret = $this->redis->set($lockKey, $requestId, ['nx', 'ex' => $expireTime]);
            if ($ret) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     * @param string $lockKey 锁key
     * @param string $requestId 客户端请求唯一标识
     * @return bool
     */
    public function releaseDistributedLock($lockKey, $requestId)
    {
        $luaScript = <<<EOF
if redis.call("get", KEYS[1]) == ARGV[1]
then
    return redis.call("del", KEYS[1])
else
    return 0
end
EOF;
        $res = $this->redis->eval($luaScript, [$lockKey, $requestId], 1);
        if ($res) {
            return true;
        }
        return false;
    }

    /**
     * Redis Hash 可重入加锁
     * @param $lockKey
     * @param $uuid
     * @param int $expireTime
     * @return bool
     */
    public function reentrantLock($lockKey, $uuid, int $expireTime = 1): bool
    {
        $luaScript = <<<EOF
-- 锁不存在，初始化值1
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('expire', KEYS[1], ARGV[1]);
    return 1;
end ;
-- 锁和键都存在，值加1
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('expire', KEYS[1], ARGV[1]);
    return 1;
end ;
return 0;
EOF;
        $res = $this->redis->eval($luaScript, [$lockKey, $uuid, $expireTime], 1);
        if ($res) {
            return true;
        }
        return false;
    }

    /**
     * Redis Hash 释放可重入锁
     * @param $lockKey
     * @param $uuid
     * @return bool
     */
    public function releaseReentrantLock($lockKey, $uuid): bool
    {
        // 1:代表解锁成功，锁被释放; 0:代表可重入次数被减1; null:代表其他线程尝试解锁，解锁失败
        $luaScript = <<<EOF
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil;
end ;
-- 计算当前可重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 小于等于 0 代表可以解锁
if (counter > 0) then
    return 0;
else
    redis.call('del', KEYS[1]);
    return 1;
end ;
return nil;
EOF;
        $res = $this->redis->eval($luaScript, [$lockKey, $uuid], 1);
        if ($res) {
            return true;
        }
        return false;
    }
}